/*
 *   AS IS License
 * ==Naive Coders==
 */
package com.naivecoders.refex.functions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import com.naivecoders.refex.FName;
import com.naivecoders.refex.FunctionFactory;
import java.util.Objects;

/**
 * In this computational model functions are used as structures to hold data and
 * as units doing computation. kinda like lambda calculus List function is
 * simple collection of disjoint functions. It doesn't have evaluation
 * semantics.
 *
 * TODO: rethink whole computational model. use composition and currying instead
 * of this.
 *
 * @author gbu.000
 *
 *
 *
 * 1=\f.\x.f x 0=\f.\x. x inc=\n.\f.\x.f (n f x)
 */
public class ListFunction extends AbstractFunction implements Iterable<AbstractFunction> {

    private final List<AbstractFunction> list = new ArrayList<>();

    public ListFunction(FunctionFactory origin) {
        super(origin);
    }

    public ListFunction(FunctionFactory origin, AbstractFunction... fs) {
        this(origin);
        list.addAll(Arrays.asList(fs));
    }

    public ListFunction(FunctionFactory origin, List<AbstractFunction> l) {
        this(origin);
        list.addAll(l);
    }

    public ListFunction(FunctionFactory origin, AbstractFunction head, ListFunction tail) {
        this(origin);
        list.add(head);
        list.addAll((tail).list);
    }

    public int size() {
        return list.size();
    }

    public FName asName() throws InvocationException {
        final FCode[] n = new FCode[list.size()];
        int i = 0;
        for (AbstractFunction af : list) {
            n[i++] = af.getCode();
            if (null == af.getCode()) {
                throw new InvocationException("cant convert to name %s", list.toString());
            }
        }
        return new FName(n);
    }

    public AbstractFunction head() throws InvocationException {
        if (list.isEmpty()) {
            throw new InvocationException("no head");
        }
        return list.get(0);
    }

    public ListFunction tail() {
        if (list.size() <= 1) {
            return getOrigin().emptyList();
        }
        final List<AbstractFunction> result = new ArrayList<>(list);
        result.remove(list.get(0));
        return getOrigin().list(result);
    }

    public boolean isEmpty() {
        return list.isEmpty();
    }

    public ListFunction append(AbstractFunction a) {
        List<AbstractFunction> result = new ArrayList<>(list);
        result.add(a);
        return getOrigin().list(result);
    }

    public ListFunction prepend(AbstractFunction a) {
        final List<AbstractFunction> result = new ArrayList<>();
        result.add(a);
        result.addAll(list);
        return getOrigin().list(result);
    }

    public ListFunction drop(int cnt) {
        List<AbstractFunction> result = new ArrayList<>();
        final Iterator<AbstractFunction> listI = list.iterator();
        for (int i = 0; i < list.size() - cnt; i++) {
            result.add(listI.next());
        }
        return getOrigin().list(result);
    }

    @Override
    public AbstractFunction evaluate(ListFunction x) throws InvocationException {
        try {
            if (size() > 1 && arg(this, 0) instanceof InvokeFunction) {
                final AbstractFunction af = arg(this, 1).evaluate(tail().tail());
                getOrigin().getTrail().registerCall(this, af);
                return af;
            }
        } catch (InvocationException ex) {
            ex.appendStackEntry(this);
            throw ex;
        }
        return this;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        dump(sb, new SingleLineIndentation());
        return sb.toString();
    }

    public String toTreeString() {
        final StringBuilder sb = new StringBuilder();
        dump(sb, new Indentation());
        return sb.toString();
    }

    private void dump(StringBuilder sb, Indentation ident) {
        ident.listStart(sb);
        for (AbstractFunction af : list) {
            if (af instanceof ListFunction) {
                ((ListFunction) af).dump(sb, ident);
            } else {
                ident.atomic(sb, af);
            }
        }
        ident.listEnd(sb);
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 53 * hash + Objects.hashCode(this.list);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final ListFunction other = (ListFunction) obj;
        if (!Objects.equals(this.list, other.list)) {
            return false;
        }
        return true;
    }

    @Override
    public Iterator<AbstractFunction> iterator() {
        return list.iterator();
    }

    public AbstractFunction get(int index) {
        return list.get(index);
    }

    private static class Indentation {

        private int indent;
        private boolean needsIndent;

        public void listStart(StringBuilder sb) {
            indent++;
            if (needsIndent) {
                indent(sb, indent);
            }
            sb.append('[');
        }

        public void listEnd(StringBuilder sb) {
            sb.append(']');
            indent(sb, indent);
            indent--;
            needsIndent = false;
        }

        public void atomic(StringBuilder sb, AbstractFunction af) {
            sb.append(af.toString());
            needsIndent = true;
        }

        protected void indent(StringBuilder sb, int indent) {
            sb.append('\n');
            for (int i = 0; i < indent; i++) {
                sb.append(' ');
            }
        }
    }

    private static class SingleLineIndentation extends Indentation {

        @Override
        protected void indent(StringBuilder sb, int indent) {
        }
    }
}
